package munci.old.know.words;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.LinkedList;

import munci.old.know.Env;
import munci.old.use.Methods;
import munci.old.use.Try;


public class MethodWord extends Word implements Comparable<MethodWord> {
	
	@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
	public @interface Name {
	    String value();
	}
	
	@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
	public @interface Compile {
	    String value();
	}
	
	public static Word.Book all(Class<?> type) {
		return all(new Word.Book(type));
	}
	
	public static Word.Book all(Word.Book list) {
		for (Word word : getWords(list.source(), null)) {
			list.add(word);
		}
		return list;
	}
	
	public static LinkedList<? extends Word> getWords(Class<?> cl, Object obj) {
		LinkedList<MethodWord> words = new LinkedList<MethodWord>();
		for (Method m : cl.getDeclaredMethods()) {
			if (m.isAnnotationPresent(MethodWord.Name.class)) {
				words.add(new MethodWord(m.getAnnotation(Name.class).value(), false, m, obj));
			}
			if (m.isAnnotationPresent(MethodWord.Compile.class)) {
				words.add(new MethodWord(m.getAnnotation(Compile.class).value(), true, m, obj));
			}
		}
		Collections.sort(words);
		return words;
	}
	
	public int compareTo(MethodWord word) {
		int cmp;
		
		cmp = (immediate? 1 : 0) - (word.immediate? 1 : 0);
		if (cmp != 0) return cmp;
		
		cmp = name.compareTo(word.name);
		if (cmp != 0) return cmp;
		
		cmp = method.getName().compareTo(word.method.getName());
		if (cmp != 0) return cmp;
		
		if (methodsAmbiguous(word.method))
			throw Try.except("Ambiguous word parameters:\n>" + this + "\n>" + word);
		
		return 0;
	}

	private boolean methodsAmbiguous(Method m1) {
		Class<?>[] types0 = Methods.paramTypes(method), types1 = Methods.paramTypes(m1);
		int i0, i1;
		
		for (i0 = types0.length - 1, i1 = types1.length - 1; i0 >= 0 && i1 >= 0; i0--, i1--) {
			if (!types0[i0].isAssignableFrom(types1[i1]) && !types1[i1].isAssignableFrom(types0[i0]))
				return false;
		}
		return true;
	}
	
	private final Method method;
	private final boolean immediate;
	private final Object obj;

	protected MethodWord(String name, boolean immediate, Method method, Object obj) {
		super(name);
		this.method = method;
		this.immediate = immediate;
		this.obj = obj;
	}

	public boolean execute(Env env) {
		boolean continues = method.getReturnType().equals(Continuation.class);
		boolean result;
		if (immediate == env.compiling()) {
			result = env.stack.invoke(method, obj);
		} else if (!immediate && !continues) {
			result = env.stack.compile(method, null);
		} else {
			return false;
		}
		if (result && continues) {
			try {
				((Continuation) env.stack.pop()).execute(env);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		return result;
	}
	
	public interface Continuation {
		void execute(Env env) throws Exception;
	}
	
	public Class<?> returnType() {
		return method.getReturnType();
	}
	
	@Override public String toString() {
		return super.toString() + 
				(immediate? " compile: " : " method: ") + 
				Methods.methodDescription(method);
	}
}
